﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace JasLib.Extensions
{
    public static class StringExtensions
    {
        /// <summary>
        /// This is small, immutable, and used frequently enough to just have a common instance.
        /// </summary>
        private static readonly string[] s_emptyStringArray = new string[0];

        #region System.String

        /**********************************************************************************/
        public static bool Contains(this string strInput, string strTest, bool bIgnoreCase)
        {
            if (bIgnoreCase)
                return (-1 != strInput.IndexOf(strTest, StringComparison.CurrentCultureIgnoreCase));
            else
                return strInput.Contains(strTest);
        }

        /**********************************************************************************/
        /// <summary>
        /// Fluent wrapper for string.Format().
        /// </summary>
        public static string FormatWith(this string strInput, params object[] aobjParams)
        {
            return string.Format(strInput, aobjParams);
        }

        /**********************************************************************************/
        /// <summary>
        /// Indicates whether the string only contains characters and digits.
        /// </summary>
        public static bool IsAlphaNumeric(this string strTest)
        {
            foreach (char chThis in strTest)
                if (!char.IsLetterOrDigit(chThis))
                    return false;
            return true;
        }

        /**********************************************************************************/
        /// <summary>
        /// Indicates whether the string is null or a System.String.Empty string.
        /// </summary>
        public static bool IsNullOrEmpty(this string strTest)
        {
            return string.IsNullOrEmpty(strTest);
        }

        /**********************************************************************************/
        public static bool IsNullOrEmptyOr(this string strTest, params string[] astrParams)
        {
            return string.IsNullOrEmpty(strTest) || strTest.In(astrParams);
        }

        /**********************************************************************************/
        /// <summary>
        /// Indicates whether the string is null, empty, or consists only of whitespace characters.
        /// </summary>
#if !DOT_NET_4_OR_LATER
        private static Regex s_WhitespaceRegex = new Regex(@"^\s*$", RegexOptions.Compiled);
#endif
        public static bool IsNullOrWhiteSpace(this string strTest)
        {
#if DOT_NET_4_OR_LATER
            return string.IsNullOrWhiteSpace(strTest);
#else
            /// .NET 4 has a framework function that does this.
            return (strTest == null) || (strTest == string.Empty) || s_WhitespaceRegex.Match(strTest).Success;
#endif
        }

        /**********************************************************************************/
        /// <summary>
        /// Provides the functionality of the SQL LIKE operator.
        /// http://bytes.com/topic/c-sharp/answers/253519-using-regex-create-sqls-like-like-function
        /// </summary>
        public static bool Like(this string strInput, string strPattern)
        {
            /* Turn "off" all regular expression related syntax in
            * the pattern string. */
            strPattern = Regex.Escape(strPattern);

            /* Replace the SQL LIKE wildcard metacharacters with the
            * equivalent regular expression metacharacters. */
            strPattern = strPattern.Replace("%", ".*?").Replace("_", ".");

            /* The previous call to Regex.Escape actually turned off
            * too many metacharacters, i.e. those which are recognized by
            * both the regular expression engine and the SQL LIKE
            * statement ([...] and [^...]). Those metacharacters have
            * to be manually unescaped here. */
            strPattern = strPattern.Replace(@"\[\^", "[^").Replace(@"\[", "[").Replace(@"\]", "]");

            /// If the caller wants substrings, that's what % is for.
            strPattern = "^" + strPattern + "$";

            return Regex.IsMatch(strInput, strPattern);
        }

        /**********************************************************************************/
        /// <summary>
        /// 
        /// </summary>
        /// <param name="iLength">
        /// If positive, this includes at most iLength characters from the left (for general truncation usage).
        /// If negative, this excludes iLength characters from the right.
        /// If zero, this returns an empty string.
        /// If the positive value is too high, this returns the original string.
        /// If the negative value is too high, this returns an empty string.
        /// </param>
        public static string Left(this string strInput, int iLength = 0)
        {
            if (iLength > strInput.Length)
                return strInput;
            else if (iLength > 0)
                return strInput.Substring(0, iLength);
            else if (-strInput.Length < iLength && iLength < 0)
                return strInput.Substring(0, strInput.Length + iLength);
            else
                return string.Empty;
        }

        /**********************************************************************************/
        /// <summary>
        /// 
        /// </summary>
        /// <param name="iLength">
        /// If positive, this includes iLength characters from the right.
        /// If negative, this excludes iLength characters from the left.
        /// If zero, this returns an empty string.
        /// If the positive value is too high, this returns the original string.
        /// If the negative value is too high, this returns an empty string.
        /// </param>
        public static string Right(this string strInput, int iLength = 0)
        {
            if (iLength > strInput.Length)
                return strInput;
            else if (iLength > 0)
                return strInput.Substring(strInput.Length - iLength);
            else if (-strInput.Length < iLength && iLength < 0)
                return strInput.Substring(-iLength);
            else
                return string.Empty;
        }

        /**********************************************************************************/
        /// <summary>
        /// Wraps Object.ToString() so that if the source object is null,
        /// then null will be returned rather than invoking a null access exception.
        /// </summary>
        public static string SafeToString(this object objValue)
        {
            if (objValue == null)
                return null;
            else
                return objValue.ToString();
        }

        /**********************************************************************************/
        /// <summary>
        /// A more convenient extension of string.Split that is null-safe
        /// and doesn't require an explicitly-allocated string array.
        /// </summary>
        public static string[] Split(this string strInput, StringSplitOptions eOptions, params string[] astrSeparators)
        {
            if (strInput == null)
                return s_emptyStringArray;

            return strInput.Split(astrSeparators, eOptions);
        }

        /**********************************************************************************/
        /// <summary>
        /// A more convenient extension of string.Split that is null-safe
        /// and doesn't require an explicitly-allocated string array.
        /// </summary>
        public static string[] Split(this string strInput, params string[] astrSeparators)
        {
            if (strInput == null)
                return s_emptyStringArray;

            /// The second parameter must be specified to resolve to the original method,
            /// otherwise it will be infinite recursion.
            return strInput.Split(astrSeparators, StringSplitOptions.None);
        }

        /**********************************************************************************/
        public static IEnumerable<string> Split(this string strInput, Func<char, bool> predicate)
        {
            StringBuilder sb = new StringBuilder(strInput.Length);

            for (int iIndex = 0; iIndex < strInput.Length; iIndex++)
            {
                char ch = strInput[iIndex];
                if (predicate(ch))
                {
                    yield return sb.ToString();
                    sb.Length = 0;
                }
                else
                    sb.Append(ch);
            }

            if (sb.Length > 0)
                yield return sb.ToString();
        }

        /**********************************************************************************/
        /// <summary>
        /// Converts the integer to a string of any arbitrary base.
        /// </summary>
        /// <param name="iValue"></param>
        /// <param name="strBaseDigitSet">A complete list of all digits in the base, from least to most significant.</param>
        /// <param name="bHasZeroPlaceholder">Whether or not the number base uses a zero value for new digits.
        /// Set this to 'false' for uses like converting a zero-based index into an Excel column name.</param>
        /// <returns></returns>
        public static string ToStringOfBase(this int iValue, string strBaseDigitSet, bool bHasZeroPlaceholder)
        {
            int iBase = strBaseDigitSet.Length;

            int iRemainder = Math.Abs(iValue);
            int iPower = iBase;

            StringBuilder sbOutput = new StringBuilder();

            do
            {
                int iDigit = iRemainder % iPower;
                sbOutput.Insert(0, strBaseDigitSet[bHasZeroPlaceholder ? iDigit : iDigit - 1]);

                iRemainder = (iRemainder - iDigit) / iPower;
                iPower *= iBase;
            }
            while (iRemainder > 0);

            if (iValue < 0)
                sbOutput.Insert(0, "-");

            return sbOutput.ToString();
        }

        /**********************************************************************************/
        /// <summary>
        /// Returns a trimmed version of the string, or promotes a null source string to string.Empty.
        /// </summary>
        /// <param name="strInput">Input string. Can be null.</param>
        public static string TrimToEmpty(this string strInput)
        {
            if (strInput == null)
                return string.Empty;

            return strInput.Trim();
        }

        /**********************************************************************************/
        /// <summary>
        /// Returns a trimmed version of the string, or null if the trimmed string was empty.
        /// Useful when an empty string presents false positives where non-null means the presence of data.
        /// </summary>
        /// <param name="strInput">Input string. Can be null.</param>
        public static string TrimToNull(this string strInput)
        {
            if (strInput == null)
                return null;

            string strTrimmed = strInput.Trim();
            if (strInput == string.Empty)
                return null;

            return strTrimmed;
        }

        #endregion

        #region System.Text.StringBuilder

        /**********************************************************************************/
        public static StringBuilder AppendFormatLine(this StringBuilder sb, string strFormat, params object[] aobjParams)
        {
            return sb.AppendFormat(strFormat, aobjParams).AppendLine();
        }

        /**********************************************************************************/
        /// <summary>
        /// Appends each string in the enumeration as a new line.
        /// </summary>
        public static StringBuilder AppendLines(this StringBuilder sb, IEnumerable<string> values)
        {
            foreach (string value in values)
                sb.AppendLine(value);
            return sb;
        }

        /**********************************************************************************/
        public static bool IsNullOrEmpty(this StringBuilder sb)
        {
            return (sb == null) || (sb.Length == 0);
        }

        #endregion

        #region IEnumerable<string>

        /**********************************************************************************/
        public static void Add(this IList<string> source, object objNewItem)
        {
            source.Add(objNewItem.ToString());
            return;
        }

        /**********************************************************************************/
        public static void AddFormat(this IList<string> source, string strFormat, params object[] aobjParams)
        {
            source.Add(string.Format(strFormat, aobjParams));
            return;
        }

        /**********************************************************************************/
        public static IEnumerable<string> DistinctNoCase(this IEnumerable<string> source)
        {
            return source.Distinct(str => str.ToLower());
        }

        /**********************************************************************************/
        public static string Join(this IEnumerable<string> enumerableInputStrings, string strSeparator = null)
        {
#if DOT_NET_4_OR_LATER
            return string.Join(strSeparator, enumerableInputStrings);
#else
            if (strSeparator == null)
                strSeparator = string.Empty;

            /// We put blind faith in the idea that string.Join() has the best implementation for arrays.
            if (enumerableInputStrings is string[])
            {
                return string.Join(strSeparator, enumerableInputStrings as string[]);
            }

            /// Arbitrary IEnumerable.
            else
            {
                /// We use a Queue as a temporary holding cell for the strings
                /// because we presume that Queue reallocations
                /// are less frequent/intense/variable than StringBuilder reallocations for the worst cases.
                Queue<string> outputQueue = new Queue<string>();

                int iFinalStringLength = 0;
                foreach (string eachString in enumerableInputStrings)
                {
                    outputQueue.Enqueue(eachString);
                    iFinalStringLength += eachString.Length;
                }

                if (outputQueue.Count == 0)
                    return string.Empty;

                iFinalStringLength += (outputQueue.Count - 1) * strSeparator.Length;

                /// Give the builder exactly the buffer size it needs so that there are no reallocations.
                StringBuilder finalBuilder = new StringBuilder(iFinalStringLength);

                bool bFirst = true;
                foreach (string eachString in outputQueue)
                {
                    if (bFirst)
                        bFirst = false;
                    else
                        finalBuilder.Append(strSeparator);

                    finalBuilder.Append(eachString);
                }

                return finalBuilder.ToString();
            }
#endif
        }

        /**********************************************************************************/
        public static bool StartsWithAny(this string strTarget, params string[] strings)
        {
            if ((strTarget != null) && (strTarget.Length > 0) && (strings != null) && (strings.Length > 0))
            {
                for (int iIndex = 0; iIndex < strings.Length; iIndex++)
                {
                    if (strTarget.StartsWith(strings[iIndex]))
                        return true;
                }
            }

            return false;
        }

        /**********************************************************************************/
        /// <summary>
        /// Unlike <seealso cref="System.String.Substring"/> which specifies a run length,
        /// this copies the substring up to and including the specified ending index.
        /// </summary>
        /// <param name="strTarget"></param>
        /// <param name="iStartIndex"></param>
        /// <param name="iEndIndex"></param>
        /// <returns></returns>
        public static string SubstringToIndex(this string strTarget, int iStartIndex, int iEndIndex = -1)
        {
            if (iStartIndex >= strTarget.Length)
                return string.Empty;

            if (iEndIndex == -1 || iEndIndex >= strTarget.Length)
                return strTarget.Substring(iStartIndex, strTarget.Length - iStartIndex);

            return strTarget.Substring(iStartIndex, iEndIndex - iStartIndex + 1);
        }

        /**********************************************************************************/
        /// <summary>
        /// Parses each string in the source enumerable and returns those that are successfully
        /// converted into integers.
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        public static IEnumerable<int> ToIntegers(this IEnumerable<string> source)
        {
            foreach (string value in source)
            {
                int iConverted = 0;
                if (int.TryParse(value, out iConverted))
                    yield return iConverted;
            }
        }

        /**********************************************************************************/
        /// <summary>
        /// Because the LINQ ToQueue() doesn't work on strings.
        /// </summary>
        public static Queue<string> ToQueue(this IEnumerable<string> source)
        {
            return new Queue<string>(source);
        }

        /**********************************************************************************/
        public static StringBuilder ToStringBuilder(this IEnumerable<string> source)
        {
            var sb = new StringBuilder();

            foreach (string str in source)
                sb.Append(str);

            return sb;
        }

        /**********************************************************************************/
        /// <summary>
        /// Converts any enumeration into a string enumeration by calling ToString() on each of the results.
        /// </summary>
        public static IEnumerable<string> ToStrings(this IEnumerable source)
        {
            foreach (object value in source)
            {
                if (value == null)
                    yield return null;
                else
                    yield return value.ToString();
            }
        }

        /**********************************************************************************/
        /// <summary>
        /// Converts any enumeration into a string enumeration by calling ToString() on each of the results.
        /// </summary>
        public static IEnumerable<string> ToStrings<Tinput>(this IEnumerable<Tinput> source)
        {
            foreach (Tinput value in source)
            {
                if (value == null)
                    yield return null;
                else
                    yield return value.ToString();
            }
        }

        /**********************************************************************************/
        /// <summary>
        /// Converts any enumeration into a string enumeration by calling ToString() on each of the results.
        /// </summary>
        public static IEnumerable<string> ToStrings<Tinput, Toutput>(this IEnumerable<Tinput> source, Func<Tinput, Toutput> func)
        {
            foreach (Tinput value in source)
            {
                if (value == null)
                    yield return null;
                else
                    yield return func.Invoke(value).ToString();
            }
        }

        /**********************************************************************************/
        /// <summary>
        /// Trims all strings and omits the ones that are null or whitespace.
        /// </summary>
        public static IEnumerable<string> WithoutWhiteSpace(this IEnumerable<string> source)
        {
            foreach (string value in source)
            {
                if (value != null)
                {
                    string strTrimmed = value.Trim();
                    if (strTrimmed != string.Empty)
                        yield return strTrimmed;
                }
            }
        }

        #endregion
    }
}